package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;


/**
 * 自定义切面，实现公共字段自动填充处理逻辑
 *
 * 后台系统的员工管理功能和菜品分类功能的开发，在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人等字段，在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段，也就是也就是在我们的系统中很多表中都会有这些字段
 *如下：create_time  创建时间，create_user  创建人id，update_time  修改时间，update_user 修改人id
 * 而针对于这些字段，我们的赋值方式为：
 * 1). 在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。
 * 2). 在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。
 *
 * 原来这些字段的编码太冗余、繁琐，对这些公共字段在某个地方统一处理：使用AOP切面编程，实现功能增强，来完成公共字段自动填充功能。
 * 实现公共字段自动填充，也就是在插入或者更新的时候为指定字段赋予指定的值，使用它的好处就是可以统一对这些字段进行处理，避免了重复代码。
 *
 * 1). 自定义注解 AutoFill，用于标识需要进行公共字段自动填充的方法
 * 2). 自定义切面类 AutoFillAspect，统一拦截加入了 AutoFill 注解的方法，通过反射为公共字段赋值
 * 3). 在 Mapper 的方法上加入 AutoFill 注解
 *
 */
@Aspect   //切面
@Component    //这也是个bean，也要交给spring容器管理
@Slf4j
public class AutoFillAspect {

    /**
     * 切入点,说明对哪些类哪些方法来实施拦截(我们要拦截的是mapper当中的某些方法)
     */
    //第一个*意思是返回值是所有的任意的。*.*意思是mapper包下的所有的类所有的方法，（..）是指匹配所有的参数类型
    //只有一个切点表达式，拦截力度太粗了。而我们只需要拦截mapper包下加入了AutoFill注解的方法，所以加一个&&表示只拦截有AutoFill的方法
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillpointCut(){}

    /**
     * 给公共字段赋值的逻辑写到通知里
     * 前置通知：因为要在执行insert或者update方法之前进行公共字段的赋值
     */
    @Before("autoFillpointCut()")   //指定切入点：当匹配上，上面那个Pointcut切点表达式的时候，才执行这个通知,指定方法名称autoFillpointCut就可以了
    //没咋看懂以下逻辑(直到赋值之前的代码)-->通过连接点joinPoint知道当前哪个方法被拦截到了，以及被拦截的方法的具体参数是什么样子的
    public void autoFill(JoinPoint joinPoint){
        //可先进行调试，是否能进入该方法 提前在mapper方法添加AutoFill注解
        log.info("自定义的AutoFillAspect切面类开始进行公共字段自动填充...");

        //1.获取到当前被拦截的方法上的数据库操作类型(update还是insert)，因为不同的类型需要填充的公共字段不同。
        //方法签名对象，并且将signature进行转型，转成signature的子接口MethodSignature
        /*
        Signature接口提供的信息是比较一般的，例如可以获取方法名，但无法获取方法的参数类型、返回类型等更详细的信息
        为了能够访问这些更详细的方法签名信息，需要将Signature对象转型为MethodSignature对象。MethodSignature是Signature的子接口
         */
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        //获得方法上的注解对象:    @AutoFill(value = OperationType.UPDATE/inset)
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);

        //获得数据库操作类型
        OperationType operationType = autoFill.value();

        //2.获取到当前被拦截的方法(update,insert方法)的参数--即实体对象
        Object[] args = joinPoint.getArgs();   // 获取了所有的参数,只要加了AutoFill注解的方法的参数
        //假设这些加了AutoFill注解的方法有不止一个参数，默认只取第一个参数，所以为了拿到employee实体参数，需要将其放在第一个的位置。eg:void update(Employee employee);
        if(args == null || args.length == 0){     //防止出现空指针
            return;
        }

        //用object类型来接收,而不是用employee来接收因为在其他mapper里面的update，insert方法的实体类型不一定是employee
        Object entity = args[0];

        //3.准备赋值的数据：时间和登录用户的id
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        //4.根据当前不同的操作类型，为对应的属性通过反射来赋值。弹幕说：entity转成了Object类型，没有getset方法，必须通过反射获得方法
        if(operationType == OperationType.INSERT) {
            //insert需要为4个公共字段赋值
            try {
                //获得entity（如employee实体类里面有@Data注解）:createTime,updateTime,createUser,updateUser的set方法，设置了相关的常量类，避免写错并且更规范，所以用的常量类
                //在本例中的所有实体类里面的createTime，updateTime都是LocalDateTime型，而createUser，updateUser都是Long型
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值
                setCreateTime.invoke(entity, now);
                setCreateUser.invoke(entity, currentId);
                setUpdateTime.invoke(entity, now);
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else if(operationType == OperationType.UPDATE){
            //update需要为两个公共字段赋值
            try{
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME,LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER,Long.class);

                //通过反射为对象属性赋值
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);
            } catch (Exception e){
                e.printStackTrace();
            }

        }

    }
}
